<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
	<title>Usermod Settings</title>
	<script src="common.js" async type="text/javascript"></script>
	<script>
	var umCfg = {};
	var pins = [], pinO = [], owner;
	var urows;
	var numM = 0;
	function S() {
		getLoc();
		// load settings and insert values into DOM
		fetch(getURL('/json/cfg'), {
			method: 'get'
		})
		.then(res => {
			if (!res.ok) gId('lserr').style.display = "inline";
			return res.json();
		})
		.then(json => {
			umCfg = json.um;
			getPins(json);
			urows="";
			if (isO(umCfg)) {
				for (const [k,o] of Object.entries(umCfg)) {
					urows += `<hr><h3>${k}</h3>`;
					addField(k,'unknown',o);
				}
			}
			if (urows==="") urows = "Usermods configuration not found.<br>Press <i>Save</i> to initialize defaults.";
			gId("um").innerHTML = urows;
			loadJS(getURL('/settings/s.js?p=8'), false, ()=>{
				d.max_gpio = 50;
				d.um_p = [];
				d.rsvd = [];
				d.ro_gpio = [];
				d.extra = [];
			}, ()=>{
				for (let r of d.rsvd) { pins.push(r); pinO.push("rsvd"); } // reserved pins
				if (d.um_p[0]==-1) d.um_p.shift();	// remove filler
				d.Sf.SDA.max = d.Sf.SCL.max = d.Sf.MOSI.max = d.Sf.SCLK.max = d.Sf.MISO.max = d.max_gpio;
				//for (let i of d.getElementsByTagName("input")) if (i.type === "number" && i.name.replace("[]","").substr(-3) === "pin") i.max = d.max_gpio;
				pinDD(); // convert INPUT to SELECT for pins
			});	// If we set async false, file is loaded and executed, then next statement is processed
		})
		.catch((error)=>{
			gId('lserr').style.display = "inline";
			console.log(error);
		});
		if (!numM) gId("um").innerHTML = "No Usermods installed.";
		if (loc) d.Sf.action = getURL('/settings/um');
	}
	function check(o,k) {   // input object, pin owner key
		/* no longer necessary with pin dropdown fields
		var n = o.name.replace("[]","").substr(-3);
		if (o.type=="number" && n.substr(0,3)=="pin") {
			for (var i=0; i<pins.length; i++) {
				if (k==pinO[i]) continue;
				if (o.value==pins[i] && pinO[i]==="if") { o.style.color="lime"; break; }
				if (o.value==pins[i] || o.value<-1 || o.value>d.max_gpio-1) { o.style.color="red"; break; } else o.style.color=d.ro_gpio.some((e)=>e==parseInt(o.value,10))?"orange":"#fff";
			}
		} else {
			switch (o.name) {
				case "SDA": break;
				case "SCL": break;
				case "MOSI": break;
				case "SCLK": break;
				case "MISO": break;
				default: return;
			}
			for (var i=0; i<pins.length; i++) {
				//if (k==pinO[i]) continue; // same owner
				if (o.value==pins[i] && pinO[i]==="if") { o.style.color="tomato"; break; }
				if (o.value==pins[i] || o.value<-1 || o.value>d.max_gpio-1) { o.style.color="red"; break; } else o.style.color=d.ro_gpio.some((e)=>e==parseInt(o.value,10))?"orange":"#fff";
			}
		}
		*/
	}
	function getPins(o) {
		if (isO(o)) {
			for (const [k,v] of Object.entries(o)) {
				if (isO(v)) {
					let oldO = owner; // keep parent name
					owner = k;
					getPins(v);
					owner = oldO;
					continue;
				}
				if (k.replace("[]","").substr(-3)=="pin") {
					if (Array.isArray(v)) {
						for (var i=0; i<v.length; i++) if (v[i]>=0) { pins.push(v[i]); pinO.push(owner); }
					} else {
						if (v>=0) { pins.push(v); pinO.push(owner); }
					}
				} else if (Array.isArray(v)) {
					for (var i=0; i<v.length; i++) getPins(v[i]);
				}
			}
		}
	}
	function initCap(s) {
  		if (typeof s !== 'string') return '';
		// https://www.freecodecamp.org/news/how-to-capitalize-words-in-javascript/
		return s.replace(/[\W_]/g,' ').replace(/(^\w{1})|(\s+\w{1})/g, l=>l.toUpperCase()); // replace - and _ with space, capitalize every 1st letter
	}
	function addField(k,f,o,a=false) {  //key, field, (sub)object, isArray
		if (isO(o)) {
			urows += '<hr class="sml">';
			if (f!=='unknown' && !k.includes(":")) urows += `<p><u>${initCap(f)}</u></p>`; //show group title
			for (const [s,v] of Object.entries(o)) {
				// possibility to nest objects (only 1 level)
				if (f!=='unknown' && !k.includes(":")) addField(k+":"+f,s,v);
				else addField(k,s,v);
			}
		} else if (Array.isArray(o)) {
			for (var j=0; j<o.length; j++) {
				addField(k,f,o[j],true);
			}
		} else {
			var c, t = typeof o;
			switch (t) {
				case "boolean":
					t = "checkbox"; c = 'value="true"' + (o ? ' checked' : '');
					break;
				case "number":
					c = `value="${o}"`;
					if (f.substr(-3)==="pin") {
						c += ` max="${d.max_gpio-1}" min="-1" class="s"`;
						t = "int";
					} else {
						c += ' step="any" class="xxl"';
					}
					break;
				default:
					t = "text"; c = `value="${o}" style="width:250px;"`;
					break;
			}
			urows += ` ${initCap(f)} `; //only show field (key is shown in grouping)
			// https://stackoverflow.com/questions/11657123/posting-both-checked-and-unchecked-checkboxes
			if (t=="checkbox") urows += `<input type="hidden" name="${k}:${f}${a?"[]":""}" value="false">`;
			else if (!a)       urows += `<input type="hidden" name="${k}:${f}${a?"[]":""}" value="${t}">`;
			urows += `<input type="${t==="int"?"number":t}" name="${k}:${f}${a?"[]":""}" ${c} oninput="check(this,'${k.substr(k.indexOf(":")+1)}')"><br>`;
		}
	}
	function pinDD() {
		for (let i of d.Sf.elements) {
			if (i.type === "number" && (i.name.includes("pin") || ["SDA","SCL","MOSI","MISO","SCLK"].includes(i.name))) { //select all pin select elements
				let v = parseInt(i.value);
				let sel = addDD(i.name,0);
				for (var j = -1; j < d.max_gpio; j++) {
					if (d.rsvd.includes(j)) continue;
					let foundPin = pins.indexOf(j);
					let txt = (j === -1) ? "unused" : `${j}`;
					if (foundPin >= 0 && j !== v) txt += ` ${pinO[foundPin]=="if"?"global":pinO[foundPin]}`; // already reserved pin
					if (d.ro_gpio.includes(j)) txt += " (R/O)";
					let opt = addO(sel, txt, j);
					if (j === v) opt.selected = true; // this is "our" pin
					else if (pins.includes(j)) opt.disabled = true; // someone else's pin
				}
				let um = i.name.split(":")[0];
				d.extra.forEach((o)=>{
					if (o[um] && o[um].pin) o[um].pin.forEach((e)=>{
						let opt = addO(sel,e[0],e[1]);
						if (e[1]==v) opt.selected = true;
					});
				});
			}
		}
	}
	function UI(e) {
		// update changed select options across all usermods
		let oldV = parseInt(e.dataset.val);
		e.dataset.val = e.value;
		let txt = e.name.split(":")[e.name.split(":").length-2];
		let selects = d.Sf.querySelectorAll("select[class='pin']");
		for (let sel of selects) {
			if (sel == e) continue
			Array.from(sel.options).forEach((i)=>{
				if (!(i.value==oldV || i.value==e.value)) return;
				if (i.value == -1) {
					i.text = "unused";
					return
				}
				if (i.value<100) { // TODO remove this hack and use d.extra
					i.text = i.value;
					if (i.value==oldV) {
						i.disabled = false;
					}
					if (i.value==e.value) {
						i.disabled = true;
						i.text += ` ${txt}`;
					}
					if (d.ro_gpio.includes(parseInt(i.value))) i.text += " (R/O)";
				}
			});
		}
	}
	// https://stackoverflow.com/questions/39729741/javascript-change-input-text-to-select-option
	function addDD(um,fld) {
		let sel = cE('select');
		if (typeof(fld) === "string") {	// parameter from usermod (field name)
			if (fld.includes("pin")) sel.classList.add("pin");
			um += ":"+fld;
		} else if (typeof(fld) === "number") sel.classList.add("pin"); // a hack to add a class
		let arr = d.getElementsByName(um);
		let idx = arr[0].type==="hidden"?1:0; // ignore hidden field
		if (arr.length > 1+idx) {
			// we have array of values (usually pins)
			for (let i of arr) {
				if (i.nodeName === "INPUT" && i.type === "number") break;
				idx++;
			}
		}
		let inp = arr[idx];
		if (inp && inp.tagName === "INPUT" && (inp.type === "text" || inp.type === "number")) {  // may also use nodeName
			let v = inp.value;
			let n = inp.name;
			// copy the existing input element's attributes to the new select element
			for (var i = 0; i < inp.attributes.length; ++ i) {
				var att = inp.attributes[i];
				// type and value don't apply, so skip them
				// ** you might also want to skip style, or others -- modify as needed **
				if (att.name != 'type' && att.name != 'value' && att.name != 'class' && att.name != 'style' && att.name != 'oninput' && att.name != 'max' && att.name != 'min') {
					sel.setAttribute(att.name, att.value);
				}
			}
			sel.setAttribute("data-val", v);
			sel.setAttribute("onchange", "UI(this)");
			// finally, replace the old input element with the new select element
			inp.parentElement.replaceChild(sel, inp);
			return sel;
		}
		return null;
	}
	var addDropdown = addDD; // backwards compatibility
	function addO(sel,txt,val) {
		if (sel===null) return; // select object missing
		let opt = cE("option");
		opt.value = val;
		opt.text = txt;
		sel.appendChild(opt);
		for (let i=0; i<sel.childNodes.length; i++) {
			let c = sel.childNodes[i];
			if (c.value == sel.dataset.val) sel.selectedIndex = i;
		}
		return opt;
	}
	var addOption = addO; // backwards compatibility
	// https://stackoverflow.com/questions/26440494/insert-text-after-this-input-element-with-javascript
	function addI(name,el,txt, txt2="") {
		let obj = d.getElementsByName(name);
		if (!obj.length) return;
		if (typeof el === "string" && obj[0]) obj[0].placeholder = el;
		else if (obj[el]) {
			if (txt!="") obj[el].insertAdjacentHTML('afterend', '&nbsp;'+txt);
			if (txt2!="") obj[el].insertAdjacentHTML('beforebegin', txt2 + '&nbsp;');  //add pre texts
		}
	}
	var addInfo = addI; // backwards compatibility
	// add Help Button
	function addHB(um) {
		addI(um + ':help',0,`<button onclick="location.href='https://kno.wled.ge/usermods/${um}'" type="button">?</button>`);
	}
	function svS(e) {
		e.preventDefault();
		if (d.Sf.checkValidity()) d.Sf.submit(); //https://stackoverflow.com/q/37323914
	}
	</script>
	<style>@import url("style.css");</style>
</head>

<body onload="S()">
	<form id="form_s" name="Sf" method="post" onsubmit="svS(event)">
		<div class="toprow">
		<div class="helpB"><button type="button" onclick="H()">?</button></div>
		<button type="button" onclick="B()">Back</button><button type="submit">Save</button><br>
		<span id="lssuc" style="color:green; display:none">&#10004; Configuration saved!</span>
		<span id="lserr" style="color:red; display:none">&#9888; Could not load configuration.</span><hr>
		</div>
		<h2>Usermod Setup</h2>
		Global I<sup>2</sup>C GPIOs (HW)<br>
		<i class="warn">(change requires reboot!)</i><br>
		SDA:<input type="number" min="-1" max="48" name="SDA" onchange="check(this,'if')" class="s" placeholder="SDA">
		SCL:<input type="number" min="-1" max="48" name="SCL" onchange="check(this,'if')" class="s" placeholder="SCL">
		<hr class="sml">
		Global SPI GPIOs (HW)<br>
		<i class="warn">(only changable on ESP32, change requires reboot!)</i><br>
		MOSI:<input type="number" min="-1" max="48" name="MOSI" onchange="check(this,'if')" class="s" placeholder="MOSI">
		MISO:<input type="number" min="-1" max="48" name="MISO" onchange="check(this,'if')" class="s" placeholder="MISO">
		SCLK:<input type="number" min="-1" max="48" name="SCLK" onchange="check(this,'if')" class="s" placeholder="SCLK">
		<hr class="sml">
		Reboot after save? <input type="checkbox" name="RBT"><br>
		<div id="um">Loading settings...</div>
		<hr><button type="button" onclick="B()">Back</button><button type="submit">Save</button>
	</form>
</body>

</html>